Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️ use playwright for e2e #3159

Open
wants to merge 79 commits into
base: main
Choose a base branch
from
Open

Conversation

thomas-lebeau
Copy link
Collaborator

@thomas-lebeau thomas-lebeau commented Nov 25, 2024

Motivation

Improve reliability and speed of our e2e tests.

Changes

We now uses Playwright as a e2e test runner. There is small changes to the API but things remains very similar.

Speed 🚀

Local CI BrowserStack*
Before ~3m50 ~8m ~24m
After ~21s ~3m75s ~9m

(*) BrowserStack timing does not account for the time spend waiting for BS resources.

Playwright already allow e2e to be faster, however there is a few other optimizations:

  • unit-bs and e2e-bs jobs run as soon as unit and e2e jobs are finished respectively. This is so we don't wait for the slower test-performance job.
  • e2e-bs job will retry each test up to two time to be more resilient for flakiness, however, the job will stop as soon as a single test fails in order to free up browserstack resources.

new useful commands:

  • yarn test:e2e --ui run playwright locally in UI mode
  • yarn test:e2e -g "unhandled rejections" to grep tests by name
  • yarn test:e2e --debug to playwright in debug mode
  • yarn test:e2e --project firefox to run tests in firefox. available projects: chromium (default), firefox, webkit, and * (all)
  • yarn test:e2e --repeat-each 3 to run each test 3 times, useful for catching flaky tests
  • BS_USERNAME=johdoe_abcD BS_ACCESS_KEY=askldjhfn yarn test:e2e:bs to run the tests in Browserstack

Important

yarn test:e2e does not build the SDK automatically, run yarn build:bundle if you have made change to the source code of the SDK.

new useful method:

  • expect([1, 2, 3]).toHaveLength(3), will show a better error message when failing, including the actual array content.
  • await page.pause() to use with --debug

Testing

Locally, run yarn test:e2e --ui

Screenshot 2024-11-25 at 10 05 22

  • Local
  • Staging
  • Unit
  • End to end

I have gone over the contributing documentation.

Copy link

cit-pr-commenter bot commented Nov 25, 2024

Bundles Sizes Evolution

📦 Bundle Name Base Size Local Size 𝚫 𝚫% Status
Rum 147.84 KiB 147.84 KiB 0 B 0.00%
Logs 51.65 KiB 51.65 KiB 0 B 0.00%
Rum Slim 106.55 KiB 106.55 KiB 0 B 0.00%
Worker 24.50 KiB 24.50 KiB 0 B 0.00%
🚀 CPU Performance
Action Name Base Average Cpu Time (ms) Local Average Cpu Time (ms) 𝚫
addglobalcontext 0.002 0.002 -0.000
addaction 0.036 0.037 0.000
addtiming 0.001 0.001 -0.000
adderror 0.053 0.052 -0.001
startstopsessionreplayrecording 0.006 0.010 0.004
startview 0.372 0.394 0.022
logmessage 0.019 0.019 0.000
🧠 Memory Performance
Action Name Base Consumption Memory (bytes) Local Consumption Memory (bytes) 𝚫 (bytes)
addglobalcontext 29.26 KiB 30.08 KiB 842 B
addaction 54.31 KiB 60.79 KiB 6.47 KiB
addtiming 27.05 KiB 29.00 KiB 1.95 KiB
adderror 58.24 KiB 62.59 KiB 4.36 KiB
startstopsessionreplayrecording 26.76 KiB 28.71 KiB 1.95 KiB
startview 422.79 KiB 427.77 KiB 4.98 KiB
logmessage 57.62 KiB 63.80 KiB 6.18 KiB

🔗 RealWorld

@thomas-lebeau thomas-lebeau force-pushed the thomas.lebeau/playwright branch from 2d0d0e3 to ba625db Compare December 23, 2024 08:30
@thomas-lebeau thomas-lebeau force-pushed the thomas.lebeau/playwright branch from 1dd0038 to 8dfc67f Compare December 23, 2024 12:02
@codecov-commenter
Copy link

codecov-commenter commented Dec 23, 2024

Codecov Report

Attention: Patch coverage is 25.00000% with 6 lines in your changes missing coverage. Please review.

Project coverage is 92.90%. Comparing base (190bdfd) to head (9ce566a).
Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
packages/rum/test/mutationPayloadValidator.ts 25.00% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3159      +/-   ##
==========================================
- Coverage   92.96%   92.90%   -0.07%     
==========================================
  Files         298      298              
  Lines        7859     7862       +3     
  Branches     1789     1792       +3     
==========================================
- Hits         7306     7304       -2     
- Misses        553      558       +5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@cy-moi cy-moi force-pushed the thomas.lebeau/playwright branch from dc360fb to faf30a2 Compare January 24, 2025 16:19
@thomas-lebeau thomas-lebeau force-pushed the thomas.lebeau/playwright branch from 894fb1b to 959889f Compare January 27, 2025 13:27
Comment on lines 24 to 27
test.info().annotations.push({
type: 'dd_tags[test.fixme]',
description: 'Unnexpected Console log message: "Ignoring unsupported entryTypes: *"',
})
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This add the property @test.fixme in CI visibility, this is very useful to create dashboards

@thomas-lebeau thomas-lebeau force-pushed the thomas.lebeau/playwright branch from c16ff1a to 195bda3 Compare February 3, 2025 07:08
@thomas-lebeau thomas-lebeau force-pushed the thomas.lebeau/playwright branch from 0e744fa to 77f9052 Compare February 6, 2025 13:56
@thomas-lebeau thomas-lebeau marked this pull request as ready for review February 12, 2025 11:06
@thomas-lebeau thomas-lebeau requested a review from a team as a code owner February 12, 2025 11:06
name: 'Firefox',
version: '91.0',
name: 'playwright-firefox',
version: '119',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ question: What is the reasoning behind this version bump?‏

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Older version of firefox don't work with this version of playwright.

The version we used to test was already chosen arbitrary and did not reflect the oldest version supported.
For unit tests however we test on the oldest browser version we support

Copy link
Member

@BenoitZugmeyer BenoitZugmeyer Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fore context: Firefox 91 was chosen because it was a ESR version. We were aware that using playwright would imply bumping a few browser versions, so using v119 looks fine to me.

Comment on lines +245 to +247
flushEvents: () => flushEvents(page),
deleteAllCookies: () => deleteAllCookies(browserContext),
sendXhr: (url: string, headers?: string[][]) => sendXhr(page, url, headers),
Copy link
Member

@BenoitZugmeyer BenoitZugmeyer Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ question: ‏What qualifies a function to be in TestContext vs used standalone by invoking it with page/browserContext?

I see we have deleteAllCookies in TestContext, but setCookie as standalone. I feel like we would scale more with standalone functions, but at the same time things might be a bit more verbose. WDYT?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! Playwright encourage to use fixtures, so adding a function to TestContext fit well with Playwright style.

However, I think there is room for both:

  • For heavily used functions, such as flushEvents, withBrowserLogs, I think it's convenient to not have to pass page or browserContext all the time.
  • For function that are less often used or used only in one test file, then I think it might be better to have standalone function.

I think deleteAllCookies is not actually a good example as it's just doing browserContext.clearCookies() so we might use that directly.

At the end, it's a bit inconsistent in this PR as multiple people have migrated tests without clear direction.

'services' in browser.options &&
browser.options.services &&
browser.options.services.some((service) => (Array.isArray(service) ? service[0] : service) === 'browserstack')
const isBrowserStack = process.env.BS_USERNAME && process.env.BS_ACCESS_KEY
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ question: ‏can we have a test that doesn't rely on the environment? Maybe using process.argv?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we certainly could detect browserstack differently here, but BS_USERNAME and BS_ACCESS_KEY are populated as env variable anyway and used for unit-bs job too.

I thought it would be convenient to use that as a signal test are running on BS.

Comment on lines 19 to 21
const filteredLogs = this.logs.filter((log) => !log.message.includes('Ignoring unsupported entryTypes: '))

interface BrowserLog {
level: string
message: string
source: string
timestamp: number
}
if (filteredLogs.length !== this.logs.length) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥜 nitpick:

if (this.logs.some((log) => log.message.includes('Ignoring unsupported entryTypes: ')) {

Copy link
Collaborator Author

@thomas-lebeau thomas-lebeau Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, we need to actually filter out these warning message, otherwise a bunch of test are failing because they expect logs to be a certain length.

Ultimately this need to be fixed in the observer as this message is shown to customers (but this seems out of scope for this PR)

nevermind, I guess it's more readable, for little performance overhead!

Comment on lines 22 to 25
test.info().annotations.push({
type: `dd_tags[test.${tag}]`,
description: value,
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥜 nitpick: ‏use addTag here

Comment on lines 7 to 19
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'android',
use: { ...devices['Pixel 7'] },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥜 nitpick:

Suggested change
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'android',
use: { ...devices['Pixel 7'] },
use: devices['Desktop Chrome'],
},
{
name: 'firefox',
use: devices['Desktop Firefox'],
},
{
name: 'webkit',
use: devices['Desktop Safari'],
},
{
name: 'android',
use: devices['Pixel 7'],

.run(async ({ intakeRegistry }) => {
const button = await getNodeInsideShadowDom('my-scrollable-div', 'button')
.run(async ({ flushEvents, intakeRegistry, page }) => {
const button = page.locator('my-scrollable-div button')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏 praise: ‏really nice to be able to locate elements within shadow dom

Comment on lines 32 to 33
const { sessionName } = test.info().project.metadata as BrowserConfiguration
test.fixme(sessionName === 'Edge', 'In Edge, the ViewportResize record data is off by almost 20px')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 suggestion: ‏we should have a single way to identify which browser is being used.

Previously, we had the getBrowserName function that returned a normalized browser name. It returned a strongly typed constant string, making it easy to understand what to expect. Now we have (at least) three ways of getting the browser name:

  • testContext.browserName === 'chromium'
  • testContext.browserName.includes("firefox")
  • test.info().project.metadata.sessionName === 'Edge'

In the same spirit, I think testContext.browserName should:

  • be strongly typed, with constant strings instead of arbitrary strings
  • support all browsers (firefox, edge, ...)
  • normalize the names (ex: playwright-firefox should be normalized to firefox)

And we should avoid having to pick into test.info() to get that information

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it is normalized (here) because browserName is a playwright property, and it's type is 'chromium' | 'firefox' | 'webkit'

About testContext.browserName.includes("firefox"), it is a mistake, I'm fixing it to testContext.browserName === 'firefox' 👌

However, Edge browsers have chromium for browserName, but the viewport issue is really on Edge, not on all chromium browsers, hence this ugly trick. 😞
I guess we could normalize browserName in TestContext differently to playwright's meaning. That would not be too much of a hassle. wdyt?

Copy link
Member

@BenoitZugmeyer BenoitZugmeyer Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

differently to playwright's meaning

Yes, I think it would be good!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this needs to be within the e2e test suite. If it makes things more complicated, we could imagine moving it somewhere else (in a standalone script).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's actually more convenient to have it run in the e2e, we avoid setting up a separate job, script, test runner, ...

})
.then((response) => response.text())
.then(resolve)
.catch(() => resolve(new Error('Fetch request failed!')))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 suggestion: ‏Don't use new Promise when you already have promises:

const rawHeaders = await page.evaluate(
        () =>
            window
              .fetch('/headers', {
                headers: [
                  ['x-foo', 'bar'],
                  ['x-foo', 'baz'],
                ],
              })
              .then((response) => response.text())
              .catch(new Error('Fetch request failed!'))
      )

(Same suggestion applies to other new Promise in that file)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants